// $Id$

// Copyright by Johannes Sixt
// This file is under GPL, the GNU General Public Licence

#include <kapplication.h>
#include <klocale.h>			/* i18n */
#include <kconfig.h>
#include <kmessagebox.h>
#include <kstatusbar.h>
#include <kfiledialog.h>
#include <q3tabdialog.h>
#include <qfile.h>
#include <QKeyEvent>
#include <QEvent>
#include "mainwndbase.h"
#include "debugger.h"
#include "gdbdriver.h"
#include "prefdebugger.h"
#include "prefmisc.h"
#include "ttywnd.h"
#include "commandids.h"
#include "valarray.h"
#ifdef HAVE_CONFIG
#include "config.h"
#endif
#include "mydebug.h"
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>			/* mknod(2) */
#endif
#define HAVE_FCNTL_H
#ifdef HAVE_FCNTL_H
#include <fcntl.h>			/* open(2) */
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>			/* getpid, unlink etc. */
#endif

#define MAX_RECENT_FILES 4

WatchWindow::WatchWindow(QWidget* parent, const char* name, Qt::WFlags f) :
	QWidget(parent, name, f),
	m_watchEdit(this, "watch_edit"),
	m_watchAdd(i18n(" Add "), this, "watch_add"),
	m_watchDelete(i18n(" Del "), this, "watch_delete"),
	m_watchVariables(this, "watch_variables"),
	m_watchV(this, 0),
	m_watchH(0)
{
	// setup the layout
	m_watchAdd.setMinimumSize(m_watchAdd.sizeHint());
	m_watchDelete.setMinimumSize(m_watchDelete.sizeHint());
	m_watchV.addLayout(&m_watchH, 0);
	m_watchV.addWidget(&m_watchVariables, 10);
	m_watchH.addWidget(&m_watchEdit, 10);
	m_watchH.addWidget(&m_watchAdd, 0);
	m_watchH.addWidget(&m_watchDelete, 0);
	
	connect(&m_watchEdit, SIGNAL(returnPressed()), SIGNAL(addWatch()));
	connect(&m_watchAdd, SIGNAL(clicked()), SIGNAL(addWatch()));
	connect(&m_watchDelete, SIGNAL(clicked()), SIGNAL(deleteWatch()));
	connect(&m_watchVariables, SIGNAL(highlighted(int)), SLOT(slotWatchHighlighted(int)));
	
	m_watchVariables.setMoveCurrentToSibling(true);
	m_watchVariables.installEventFilter(this);
}

WatchWindow::~WatchWindow()
{
}

bool WatchWindow::eventFilter(QObject*, QEvent* ev)
{
	if (ev->type() == QEvent::KeyPress)
	{
		QKeyEvent* kev = static_cast<QKeyEvent*>(ev);
		if (kev->key() == Qt::Key_Delete) 
		{
			emit deleteWatch();
			return true;
		}
	}
	return false;
}


// place the text of the hightlighted watch expr in the edit field
void WatchWindow::slotWatchHighlighted(int idx)
{
	QString text = m_watchVariables.exprStringAt(idx);
	m_watchEdit.setText(text);
}


static void splitCmdStr(const QString& cmd, ValArray<QString>& parts)
{
	QString str = cmd.simplified();
	int start = 0;
	int end;
	while ((end = str.find(' ', start)) >= 0) 
	{
		parts.append(str.mid(start, end-start));
		start = end+1;
	}
	parts.append(str.mid(start, str.length()-start));
}


const char defaultTermCmdStr[] = "xterm -name kdbgio -title %T -e sh -c %C";
const char defaultSourceFilter[] = "*.c *.cc *.cpp *.c++ *.C *.CC";
const char defaultHeaderFilter[] = "*.h *.hh *.hpp *.h++";


DebuggerMainWndBase::DebuggerMainWndBase() :
	m_outputTermCmdStr(defaultTermCmdStr),
	m_outputTermProc(0),
	m_ttyLevel(-1),			/* no tty yet */
#ifdef GDB_TRANSCRIPT
	m_transcriptFile(GDB_TRANSCRIPT),
#endif
	m_popForeground(false),
	m_backTimeout(1000),
	m_tabWidth(0),
	m_sourceFilter(defaultSourceFilter),
	m_headerFilter(defaultHeaderFilter),
	m_debugger(0)
{
	m_statusActive = i18n("active");
}

DebuggerMainWndBase::~DebuggerMainWndBase()
{
	delete m_debugger;
	
	// if the output window is open, close it
	if (m_outputTermProc != 0) 
	{
		m_outputTermProc->disconnect();	/* ignore signals */
		m_outputTermProc->kill();
		shutdownTermWindow();
	}
}

void DebuggerMainWndBase::setupDebugger(QWidget* parent,
					ExprWnd* localVars,
					ExprWnd* watchVars,
					Q3ListBox* backtrace)
{
	m_debugger = new KDebugger(parent, localVars, watchVars, backtrace);
	
	QObject::connect(m_debugger, SIGNAL(updateStatusMessage()),
				parent, SLOT(slotNewStatusMsg()));
	QObject::connect(m_debugger, SIGNAL(updateUI()),
				parent, SLOT(updateUI()));
	
	QObject::connect(m_debugger, SIGNAL(breakpointsChanged()),
				parent, SLOT(updateLineItems()));
	
	QObject::connect(m_debugger, SIGNAL(debuggerStarting()),
				parent, SLOT(slotDebuggerStarting()));
}


void DebuggerMainWndBase::setCoreFile(const QString& corefile)
{
	assert(m_debugger != 0);
	m_debugger->useCoreFile(corefile, true);
}

void DebuggerMainWndBase::setRemoteDevice(const QString& remoteDevice)
{
	if (m_debugger != 0) 
	{
		m_debugger->setRemoteDevice(remoteDevice);
    }
}

void DebuggerMainWndBase::setTranscript(const char* name)
{
	m_transcriptFile = name;
	if (m_debugger != 0 && m_debugger->driver() != 0)
	m_debugger->driver()->setLogFileName(m_transcriptFile);
}

const char OutputWindowGroup[] = "OutputWindow";
const char TermCmdStr[] = "TermCmdStr";
const char KeepScript[] = "KeepScript";
const char DebuggerGroup[] = "Debugger";
const char DebuggerCmdStr[] = "DebuggerCmdStr";
const char PreferencesGroup[] = "Preferences";
const char PopForeground[] = "PopForeground";
const char BackTimeout[] = "BackTimeout";
const char TabWidth[] = "TabWidth";
const char FilesGroup[] = "Files";
const char SourceFileFilter[] = "SourceFileFilter";
const char HeaderFileFilter[] = "HeaderFileFilter";
const char GeneralGroup[] = "General";

void DebuggerMainWndBase::saveSettings(KConfig* config)
{
	if (m_debugger != 0) 
	{
		m_debugger->saveSettings(config);
	}
	
	KConfigGroup g(config, OutputWindowGroup);
	g.writeEntry(TermCmdStr, m_outputTermCmdStr);
	
	config->setGroup(DebuggerGroup);
	g.writeEntry(DebuggerCmdStr, m_debuggerCmdStr);
	
	config->setGroup(PreferencesGroup);
	g.writeEntry(PopForeground, m_popForeground);
	g.writeEntry(BackTimeout, m_backTimeout);
	g.writeEntry(TabWidth, m_tabWidth);
	g.writeEntry(SourceFileFilter, m_sourceFilter);
	g.writeEntry(HeaderFileFilter, m_headerFilter);
}

void DebuggerMainWndBase::restoreSettings(KConfig* config)
{
	if (m_debugger != 0) 
	{
		m_debugger->restoreSettings(config);
	}
	
	KConfigGroup g(config, OutputWindowGroup);
	/*
	* For debugging and emergency purposes, let the config file override
	* the shell script that is used to keep the output window open. This
	* string must have EXACTLY 1 %s sequence in it.
	*/
	setTerminalCmd(g.readEntry(TermCmdStr, defaultTermCmdStr));
	m_outputTermKeepScript = g.readEntry(KeepScript);
	
	config->setGroup(DebuggerGroup);
	setDebuggerCmdStr(g.readEntry(DebuggerCmdStr));
	
	config->setGroup(PreferencesGroup);
	m_popForeground = g.readEntry(PopForeground, false);
	m_backTimeout = g.readEntry(BackTimeout, 1000);
	m_tabWidth = g.readEntry(TabWidth, 0);
	m_sourceFilter = g.readEntry(SourceFileFilter, m_sourceFilter);
	m_headerFilter = g.readEntry(HeaderFileFilter, m_headerFilter);
}

void DebuggerMainWndBase::setAttachPid(const QString& pid)
{
	assert(m_debugger != 0);
	m_debugger->setAttachPid(pid);
}

bool DebuggerMainWndBase::debugProgram(const QString& executable,
				       QString lang, QWidget* parent)
{
	assert(m_debugger != 0);
	
	TRACE(QString().sprintf("trying language '%s'...", lang));
	DebuggerDriver* driver = driverFromLang(lang);
	
	if (driver == 0)
	{
		// see if there is a language in the per-program config file
		QString configName = m_debugger->getConfigForExe(executable);
		if (QFile::exists(configName))
		{
			KConfig c(configName, KConfig::OnlyLocal);
			c.setGroup(GeneralGroup);
		
			// Using "GDB" as default here is for backwards compatibility:
			// The config file exists but doesn't have an entry,
			// so it must have been created by an old version of KDbg
			// that had only the GDB driver.
			lang = c.readEntry(KDebugger::DriverNameEntry, "GDB").latin1();
		
			TRACE(QString().sprintf("...bad, trying config driver %s...",
					lang.data()));
			driver = driverFromLang(lang);
		}
		
	}
	if (driver == 0)
	{
		QString name = driverNameFromFile(executable);
		
		TRACE(QString().sprintf("...no luck, trying %s derived"
					" from file contents", name));
		driver = driverFromLang(name);
	}
	if (driver == 0)
	{
		// oops
		QString msg = i18n("Don't know how to debug language `%1'");
		KMessageBox::sorry(parent, msg.arg(lang));
		return false;
	}
	
	driver->setLogFileName(m_transcriptFile);
	
	bool success = m_debugger->debugProgram(executable, driver);
	
	if (!success)
	{
		delete driver;
		
		QString msg = i18n("Could not start the debugger process.\n"
					"Please shut down KDbg and resolve the problem.");
		KMessageBox::sorry(parent, msg);
	}
	
	return success;
}

// derive driver from language
DebuggerDriver* DebuggerMainWndBase::driverFromLang(const QString& _lang)
{
    // lang is needed in all lowercase
    const char* lang = _lang.lower().toAscii().constData();

    // The following table relates languages and debugger drivers
    static const struct L {
		const char* shortest;	// abbreviated to this is still unique
		const char* full;	// full name of language
		int driver;
    } langs[] = {
	{ "c",       "c++",     1 },
	{ "f",       "fortran", 1 },
	{ "p",       "python",  3 },
	{ "x",       "xslt",    2 },
	// the following are actually driver names
	{ "gdb",     "gdb",     1 },
	{ "xsldbg",  "xsldbg",  2 },
	};
	const int N = sizeof(langs)/sizeof(langs[0]);
	
	// lookup the language name
	int driverID = 0;
	for (int i = 0; i < N; i++)
	{
		const L& l = langs[i];
		
		// shortest must match
		if (strncmp(l.shortest, lang, strlen(l.shortest)) != 0)
			continue;
		
		// lang must not be longer than the full name, and it must match
		if (strlen(lang) <= strlen(l.full) &&  strncmp(l.full, lang, strlen(lang)) == 0)
		{
			driverID = l.driver;
			break;
		}
    }
	DebuggerDriver* driver = 0;
	switch (driverID) 
	{
		case 1:
			{
				GdbDriver* gdb = new GdbDriver;
				gdb->setDefaultInvocation(m_debuggerCmdStr);
				driver = gdb;
			}
			break;
		default:
			// unknown language
			break;
	}
	return driver;
}

/**
 * Try to guess the language to use from the contents of the file.
 */
QString DebuggerMainWndBase::driverNameFromFile(const QString& exe)
{
	/* Inprecise but simple test to see if file is in XSLT language */
	if (exe.right(4).lower() == ".xsl")
		return "XSLT";
	
	return "GDB";
}

// helper that gets a file name (it only differs in the caption of the dialog)
QString DebuggerMainWndBase::myGetFileName(QString caption,
			   QString dir, QString filter,
			   QWidget* parent)
{
	QString filename;
	KFileDialog dlg(KUrl(dir), filter, parent);
	
	dlg.setCaption(caption);
	
	if (dlg.exec() == QDialog::Accepted)
		filename = dlg.selectedFile();
	
	return filename;
}

void DebuggerMainWndBase::newStatusMsg(KStatusBar* statusbar)
{
	QString msg = m_debugger->statusMessage();
	statusbar->changeItem(msg, ID_STATUS_MSG);
}

void DebuggerMainWndBase::doGlobalOptions(QWidget* parent)
{
	Q3TabDialog dlg(parent, "global_options", true);
	QString title = KGlobal::caption();
	title += i18n(": Global options");
	dlg.setCaption(title);
	dlg.setCancelButton(i18n("Cancel"));
	dlg.setOKButton(i18n("OK"));
	
	PrefDebugger prefDebugger(&dlg);
	prefDebugger.setDebuggerCmd(m_debuggerCmdStr.isEmpty()  ?
				GdbDriver::defaultGdb()  :  m_debuggerCmdStr);
	prefDebugger.setTerminal(m_outputTermCmdStr);
	
	PrefMisc prefMisc(&dlg);
	prefMisc.setPopIntoForeground(m_popForeground);
	prefMisc.setBackTimeout(m_backTimeout);
	prefMisc.setTabWidth(m_tabWidth);
	prefMisc.setSourceFilter(m_sourceFilter);
	prefMisc.setHeaderFilter(m_headerFilter);
	
	dlg.addTab(&prefDebugger, i18n("&Debugger"));
	dlg.addTab(&prefMisc, i18n("&Miscellaneous"));
	if (dlg.exec() == QDialog::Accepted)
	{
		setDebuggerCmdStr(prefDebugger.debuggerCmd());
		setTerminalCmd(prefDebugger.terminal());
		m_popForeground = prefMisc.popIntoForeground();
		m_backTimeout = prefMisc.backTimeout();
		m_tabWidth = prefMisc.tabWidth();
		m_sourceFilter = prefMisc.sourceFilter();
		if (m_sourceFilter.isEmpty())
			m_sourceFilter = defaultSourceFilter;
		m_headerFilter = prefMisc.headerFilter();
		if (m_headerFilter.isEmpty())
			m_headerFilter = defaultHeaderFilter;
	}
}

const char fifoNameBase[] = "/tmp/kdbgttywin%05d";

/*
 * We use the scope operator :: in this function, so that we don't
 * accidentally use the wrong close() function (I've been bitten ;-),
 * outch!) (We use it for all the libc functions, to be consistent...)
 */
QString DebuggerMainWndBase::createOutputWindow()
{
	// create a name for a fifo
	QString fifoName;
	fifoName.sprintf(fifoNameBase, ::getpid());

	// create a fifo that will pass in the tty name
	::unlink(fifoName.toAscii().constData());			/* remove remnants */
#ifdef HAVE_MKFIFO
	if (::mkfifo(fifoName.toAscii().constData(), S_IRUSR|S_IWUSR) < 0) 
	{
		// failed
		TRACE("mkfifo " + fifoName + " failed");
		return QString();
	}
#else
	if (::mknod(fifoName.toAscii().constData(), S_IFIFO | S_IRUSR|S_IWUSR, 0) < 0) 
	{
		// failed
		TRACE("mknod " + fifoName + " failed");
		return QString();
	}
#endif

	m_outputTermProc = new K3Process;

	{
		/*
		* Spawn an xterm that in turn runs a shell script that passes us
		* back the terminal name and then only sits and waits.
		*/
		static const char shellScriptFmt[] =
			"tty>%s;"
			"trap \"\" INT QUIT TSTP;"	/* ignore various signals */
			"exec<&-;exec>&-;"		/* close stdin and stdout */
			"while :;do sleep 3600;done";
		// let config file override this script
		const char* fmt = shellScriptFmt;
		if (m_outputTermKeepScript.length() != 0) 
		{
			fmt = m_outputTermKeepScript.toAscii().constData();
		}
	
		QString shellScript;
		shellScript.sprintf(fmt, fifoName.toAscii().constData());
		TRACE("output window script is " + shellScript);
	
		QString title = KGlobal::caption();
		title += i18n(": Program output");
	
		// parse the command line specified in the preferences
		ValArray<QString> cmdParts;
		splitCmdStr(m_outputTermCmdStr, cmdParts);
	
		/*
		* Build the argv array. Thereby substitute special sequences:
		*/
		struct {
			char seq[4];
			QString replace;
		} substitute[] = {
			{ "%T", title },
			{ "%C", shellScript }
		};
	
		for (int i = 0; i < cmdParts.size(); i++) 
		{
			QString& str = cmdParts[i];
			for (int j = sizeof(substitute)/sizeof(substitute[0])-1; j >= 0; j--) 
			{
				int pos = str.find(substitute[j].seq);
				if (pos >= 0) 
				{
					str.replace(pos, 2, substitute[j].replace);
					break;		/* substitute only one sequence */
				}
			}
			*m_outputTermProc << str;
		}
	
	}

	if (m_outputTermProc->start())
	{
		// read the ttyname from the fifo
		int f = ::open(fifoName.toAscii().constData(), O_RDONLY);
		if (f < 0) 
		{
			// error
			::unlink(fifoName.toAscii().constData());
			return QString();
		}
	
		char ttyname[50];
		int n = ::read(f, ttyname, sizeof(ttyname)-sizeof(char));   /* leave space for '\0' */
	
		::close(f);
		::unlink(fifoName.toAscii().constData());
	
		if (n < 0) 
		{
			// error
			return QString();
		}
	
		// remove whitespace
		ttyname[n] = '\0';
		QString tty = QString(ttyname).trimmed();
		TRACE("tty=" + tty);
		return tty;
	}
	else
	{
		// error, could not start xterm
		TRACE("fork failed for fifo " + fifoName);
		::unlink(fifoName.toAscii().constData());
		shutdownTermWindow();
		return QString();
	}
}

void DebuggerMainWndBase::shutdownTermWindow()
{
	delete m_outputTermProc;
	m_outputTermProc = 0;
}

void DebuggerMainWndBase::setTerminalCmd(const QString& cmd)
{
	m_outputTermCmdStr = cmd;
	// revert to default if empty
	if (m_outputTermCmdStr.isEmpty()) 
	{
		m_outputTermCmdStr = defaultTermCmdStr;
	}
}

void DebuggerMainWndBase::slotDebuggerStarting()
{
	if (m_debugger == 0)		/* paranoia check */
		return;
	
	if (m_ttyLevel == m_debugger->ttyLevel())
	{
	}
	else
	{
		// shut down terminal emulations we will not need
		switch (m_ttyLevel) 
		{
			case KDebugger::ttySimpleOutputOnly:
				ttyWindow()->deactivate();
				break;
			case KDebugger::ttyFull:
				if (m_outputTermProc != 0) 
				{
					m_outputTermProc->kill();
					// will be deleted in slot
				}
				break;
			default: 
				break;
	}
	
	m_ttyLevel = m_debugger->ttyLevel();
	
	QString ttyName;
	switch (m_ttyLevel) 
	{
		case KDebugger::ttySimpleOutputOnly:
			ttyName = ttyWindow()->activate();
			break;
		case KDebugger::ttyFull:
			if (m_outputTermProc == 0) 
			{
				// create an output window
				ttyName = createOutputWindow();
				TRACE(ttyName.isEmpty() ?
						"createOuputWindow failed" : "successfully created output window");
			}
			break;
		default: 
			break;
	}
	
	m_debugger->setTerminal(ttyName);
	}
}

void DebuggerMainWndBase::setDebuggerCmdStr(const QString& cmd)
{
	m_debuggerCmdStr = cmd;
	// make empty if it is the default
	if (m_debuggerCmdStr == GdbDriver::defaultGdb()) 
	{
		m_debuggerCmdStr = QString();
	}
}


#include "mainwndbase.moc"
